package net.oschina.allchat.message.processing.json.parsers.convention;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import net.oschina.allchat.message.processing.Attribute;
import net.oschina.allchat.message.processing.BadMessageException;
import net.oschina.allchat.message.processing.IElementParser;
import net.oschina.allchat.message.processing.IParsingContext;
import net.oschina.allchat.message.processing.ParserAdaptor;
import net.oschina.allchat.message.processing.ParsingPath;
import net.oschina.allchat.message.processing.convention.annotations.Array;
import net.oschina.allchat.message.processing.convention.annotations.BindTo;
import net.oschina.allchat.message.processing.convention.annotations.EmbeddedObject;
import net.oschina.allchat.message.processing.convention.annotations.NotBind;
import net.oschina.allchat.message.processing.convention.annotations.Text;
import net.oschina.allchat.message.processing.convention.annotations.TextOnly;
import net.oschina.allchat.message.processing.conversion.ConversionException;
import net.oschina.allchat.message.processing.conversion.ConversionFactory;
import net.oschina.allchat.message.processing.conversion.Converter;
import net.oschina.allchat.message.processing.conversion.IConversionFactory;
import net.oschina.allchat.message.processing.conversion.IConverter;
import net.oschina.allchat.message.processing.conversion.IConverterFactory;
import net.oschina.allchat.message.processing.validation.Validator;
import net.oschina.allchat.protocol.Constants;
import net.oschina.allchat.protocol.core.Protocol;

public class NamingConventionParser<T> extends ParserAdaptor<T> {
	private static final Object KEY_PARSING_OBJECTS_STACK = new Object();
	
	private Map<String, IElementAccessor> elementAccessors;
	private Map<String, PropertyDescriptor> embeddedObjectAccessors;
	
	private static IConversionFactory conversionFactory = new ConversionFactory();
	
	public NamingConventionParser(Class<T> objectType) {
		super(objectType);
		embeddedObjectAccessors = new HashMap<String, PropertyDescriptor>();
	}
	
	@Override
	public IElementParser<T> getElementParser(String sParsingPath) {
		if (elementAccessors != null) {
			return doGetElementParser(sParsingPath);
		}
		
		synchronized (this) {
			if (elementAccessors != null)
				return doGetElementParser(sParsingPath);
			
			elementAccessors = new HashMap<String, IElementAccessor>();
			ParsingPath parsingPath = new ParsingPath();
			try {
				scanType(parsingPath, new DefaultElementAccessor(objectType, null));
			} catch (IntrospectionException e) {
				throw new RuntimeException("failed to get element parser", e);
			}
		}
		
		return doGetElementParser(sParsingPath);
	}
	
	private IElementParser<T> doGetElementParser(String sParsingPath) {
		IElementAccessor elementAccessor = elementAccessors.get(sParsingPath);
		if (elementAccessor == null) {
			return null;
		}
		
		return new DefaultElementParser<T>(elementAccessor);
	}
	
	private void scanType(ParsingPath parsingPath, IElementAccessor elementAccessor) throws IntrospectionException {
		Class<?> elementType = elementAccessor.getType();
		
		if (elementType == null)
			return;
		
		nextField:
		for (Field field : elementType.getDeclaredFields()) {
			PropertyDescriptor propertyDescriptor = null;
			try {
				propertyDescriptor = new PropertyDescriptor(field.getName(), elementType);
			} catch (IntrospectionException e) {
				continue;
			}
			
			if (propertyDescriptor.getReadMethod() == null || propertyDescriptor.getWriteMethod() == null)
				continue;
			
			Text text = null;
			EmbeddedObject embeddedObject = null;
			BindTo bindTo = null;
			Array array = null;
			TextOnly textOnly = null;
			IConverter converter = null;
			
			Annotation[] annotations = field.getAnnotations();
			for (Annotation annotation : annotations) {
				Class<?> annotationType = annotation.annotationType();
				if (annotationType == NotBind.class)
					continue nextField;
				
				if (annotationType == Text.class) {
					text = (Text)annotation;
				} else if (annotationType == EmbeddedObject.class) {
					embeddedObject = (EmbeddedObject)annotation;
				} else if (annotationType == BindTo.class) {
					bindTo = (BindTo)annotation;
				} else if (annotationType == Array.class) {
					array = (Array)annotation;
				} else if (annotationType == TextOnly.class) {
					textOnly = (TextOnly)annotation;
				} else if (annotation.annotationType().getAnnotation(Converter.class) != null) {
					converter = conversionFactory.getConverter(annotation);
				} else if (annotation.annotationType().getAnnotation(Validator.class) != null) {
					// TODO add validation support
				}
			}
			
			String name = null;
			if (bindTo != null) {
				name = bindTo.value();
			}
			
			if (name == null) {
				if (text != null) {
					name = Constants.SYMBOL_FIELD_TEXT;
				} else {
					name = getNameByNamingConvention(field.getName());
				}
			}
				
			Class<?> fieldType = field.getType();
			if (isTextField(text, fieldType, elementType)) {
				elementAccessor.putPropertyAccessor(name, new DefaultPropertyAccessor(propertyDescriptor));
				elementAccessor.setTextObjectName(name);
			} else if (isTextOnly(textOnly) && isStringType(fieldType)) {
				parsingPath.enter(name);
				elementAccessors.put(parsingPath.toString(), new TextOnlyElementAccessor(propertyDescriptor, name));
				parsingPath.exit();
			} else if (isEmbeddedObject(embeddedObject)) {
				if (!name.startsWith(Constants.PREFIX_PROTOCOL_OBJECT)) {
					name = Constants.PREFIX_PROTOCOL_OBJECT + name;
				}
				
				embeddedObjectAccessors.put(name, propertyDescriptor);
			} else if (isPrimitiveType(fieldType)) {
				elementAccessor.putPropertyAccessor(name, new DefaultPropertyAccessor(propertyDescriptor));
			} else if (isConvertible(converter)) {
				elementAccessor.putPropertyAccessor(name, new DefaultPropertyAccessor(propertyDescriptor));
				elementAccessor.putConverter(name, converter);
			} else if (isArray(array, fieldType)) {
				parsingPath.enter(name);
				elementAccessors.put(parsingPath.toString(), new DefaultElementAccessor(ArrayList.class, propertyDescriptor));
				scanArrayElementType(parsingPath, array.value(), textOnly);					
				parsingPath.exit();
			} else {
				parsingPath.enter(name);
				scanType(parsingPath, new DefaultElementAccessor(field.getType(), propertyDescriptor));
				parsingPath.exit();
			}
		}
		
		elementAccessors.put(parsingPath.toString(), elementAccessor);
	}

	private boolean isConvertible(IConverter converter) {
		return converter != null;
	}

	private boolean isStringType(Class<?> fieldType) {
		return String.class.equals(fieldType);
	}

	private boolean isTextOnly(TextOnly textOnly) {
		return textOnly != null;
	}
	
	private void scanArrayElementType(ParsingPath parsingPath, Class<?> arrayElementType,  TextOnly textOnly) throws IntrospectionException {
		parsingPath.enter(Constants.NAME_ARRAY_ELEMENT);
		if (isTextOnly(textOnly)) {
			elementAccessors.put(parsingPath.toString(), new TextOnlyArrayElementAccessor());
		} else {
			scanType(parsingPath, new ArrayElementAccessor(arrayElementType));
		}
		parsingPath.exit();
	}
	
	private boolean isArray(Array array, Class<?> fieldType) {
		return List.class.isAssignableFrom(fieldType) && array != null;
	}

	private boolean isTextField(Text text, Class<?> fieldType, Class<?> elementType) {
		if (text != null) {
			if (!fieldType.equals(String.class)) {
				throw new BadMessageException(String.format("text field %s.%s should be string type",
						elementType.getName(), fieldType.getName()));
			}
			
			return true;
		} else {
			return false;
		}
	}

	private boolean isEmbeddedObject(EmbeddedObject embeddedObject) {
		return embeddedObject != null;
	}
		
	private class DefaultElementParser<V> implements IElementParser<V> {
		private IElementAccessor elementAccessor;
		
		public DefaultElementParser(IElementAccessor elementAccessor) {
			this.elementAccessor = elementAccessor;
		}

		@Override
		public void processAttributes(IParsingContext<V> context, List<Attribute<?>> attributes) {
			Object parsingObject = createParsingObject(context, elementAccessor);
			
			for (Attribute<?> attribute : attributes) {
				String attributeName = attribute.getLocalName();
				if (attribute.getPrefix() != null) {
					attributeName = attribute.getPrefix() + Constants.TOKEN_NAMESPACE + attributeName;
				}
				
				IPropertyAccessor pa = elementAccessor.getPropertyAccessor(attributeName);
				if (pa == null) {
					throw new BadMessageException(String.format("attribute '%s' not allowed in here: %s",
							attributeName, context));
				}
				
				// TODO validation and conversion
				Object value = null;
				try {
					value = convertValue(attributeName, attribute.getValue());
				} catch (ConversionException e) {
					throw new BadMessageException(String.format("can't convert attribute'[%s:%s]' value. parsing context: %s",
							attribute.getPrefix(), attribute.getLocalName(), context), e);
				}
				
				pa.write(context, parsingObject, value);
			}
		}

		private Object convertValue(String name, Object value) throws ConversionException {
			IConverter converter = elementAccessor.getConverter(name);
			if (converter != null) {
				return converter.from(value);
			} else {
				return value;
			}
		}
		
		@Override
		public void processText(IParsingContext<V> context, String text) {
			Object parsingObject = getParsingObject(context, elementAccessor);
			
			if (text != null) {
				String textObjectName = elementAccessor.getTextObjectName();
				
				// TODO validation and conversion
				Object value = null;
				try {
					value = convertValue(textObjectName, text);
				} catch (ConversionException e) {
					throw new BadMessageException(String.format("can't convert text value. parsing context: %s",
							context), e);
				}
				
				IPropertyAccessor pa = elementAccessor.getPropertyAccessor(textObjectName);
				if (pa == null) {
					throw new BadMessageException(String.format("can't get property accessor for text. parsing context: %s",
							context));
				}
				
				pa.write(context, parsingObject, value);
			}
				
			Object removed = removeParsingObject(context, elementAccessor);
			if (removed == context.getObject()) { // root object removed
				context.removeAttribute(KEY_PARSING_OBJECTS_STACK);
			} else {
				Object parentObject = getParsingObject(context, elementAccessor);
				if (parentObject != null) {
					elementAccessor.write(context, parentObject, parsingObject);
				}
			}
		}
		
		private Object removeParsingObject(IParsingContext<V> context, IElementAccessor elementAccessor) {
			Stack<Object> stack = context.getAttribute(KEY_PARSING_OBJECTS_STACK);
			if (stack == null) {
				return null;
			}
			
			if (elementAccessor.getType() == null) {
				// it must be a text only element. text element doesn't create a parsing object.
				return null;
			}
			
			return stack.pop();
		}
		
		private Object createParsingObject(IParsingContext<V> context, IElementAccessor elementAccessor) {
			Stack<Object> stack = context.getAttribute(KEY_PARSING_OBJECTS_STACK);
			if (stack == null) {
				stack = new Stack<Object>();
				context.setAttribute(KEY_PARSING_OBJECTS_STACK, stack);
				stack.push(context.getObject());
			} else {
				Object object = elementAccessor.create();
				if (object != null) {
					stack.push(object);
				}
			}
			
			return stack.peek();
		}
		
		private Object getParsingObject(IParsingContext<V> context, IElementAccessor elementAccessor) {
			Stack<Object> stack = context.getAttribute(KEY_PARSING_OBJECTS_STACK);
			return stack.peek();
		}
		
	}
	
	private String getNameByNamingConvention(String name) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0 ; i < name.length(); i++) {
			char c = name.charAt(i);
			if (Character.isUpperCase(c)) {
				sb.append('-').append(Character.toLowerCase(c));
			} else {
				sb.append(c);
			}
		}
		
		if (sb.length() > 1 && sb.charAt(0) == '-') {
			sb.delete(0, 1);
		}
		
		return sb.toString();
	}

	private boolean isPrimitiveType(Class<?> fieldType) {
		return fieldType.equals(String.class) ||
				fieldType.equals(boolean.class) ||
				fieldType.equals(Boolean.class) ||
				fieldType.equals(int.class) ||
				fieldType.equals(Integer.class) ||
				fieldType.equals(float.class) ||
				fieldType.equals(Float.class) ||
				fieldType.equals(double.class) ||
				fieldType.equals(Double.class) ||
				fieldType.equals(BigInteger.class) ||
				fieldType.equals(BigDecimal.class);
	}

	@Override
	public void processEmbeddedObject(IParsingContext<T> context, Protocol protocol, Object embedded) {
		if (embedded == null) {
			return;
		}
		
		PropertyDescriptor pd = embeddedObjectAccessors.get(Constants.PREFIX_PROTOCOL_OBJECT + protocol.getLocalName());
		if (pd == null) {
			throw new BadMessageException(String.format("embedded object type %s not allowed in class %s",
					embedded.getClass(), context.getObject()));
		}
		
		Method writter = pd.getWriteMethod();
		try {
			writter.invoke(context.getObject(), new Object[]{embedded});
		} catch (Exception e) {
			throw new RuntimeException(String.format("failed to invoke write method '%s'. current object: %s",
					pd.getName(), context.getObject().getClass().getName()), e);
		}
	}
	
	public static <T extends Annotation> void register(Class<T> type, IConverterFactory<T> converterFactory) {
		conversionFactory.register(type, converterFactory);
	}
	
	public static <T extends Annotation> IConverterFactory<T> unregister(Class<T> type) {
		return conversionFactory.unregister(type);
	}
	
	private static interface IElementAccessor {
		void putConverter(String name, IConverter converter);
		IConverter getConverter(String name);
		void putPropertyAccessor(String name, IPropertyAccessor propertyAccessor);
		IPropertyAccessor getPropertyAccessor(String name);
		void setTextObjectName(String textObjectName);
		String getTextObjectName();
		public Class<?> getType();
		public Object create();
		public void write(IParsingContext<?> context, Object parent, Object element);
	}
	
	private static class DefaultElementAccessor implements IElementAccessor {
		private String textObjectName;
		private Map<String, IPropertyAccessor> propertyAccessors;
		private Map<String, IConverter> converters;
		private PropertyDescriptor ownAccessor;
		private Class<?> type;
		
		public DefaultElementAccessor(Class<?> type, PropertyDescriptor ownAccessor) {
			this.type = type;
			this.ownAccessor = ownAccessor;
			propertyAccessors = new HashMap<String, IPropertyAccessor>();
			converters = new HashMap<String, IConverter>();
		}
		
		@Override
		public void putConverter(String name, IConverter converter) {
			converters.put(name, converter);
		}
		
		@Override
		public IConverter getConverter(String name) {
			return converters.get(name);
		}
		
		@Override
		public void putPropertyAccessor(String name, IPropertyAccessor propertyAccessor) {
			propertyAccessors.put(name, propertyAccessor);
		}
		
		@Override
		public IPropertyAccessor getPropertyAccessor(String name) {
			return propertyAccessors.get(name);
		}
		
		@Override
		public void setTextObjectName(String textObjectName) {
			this.textObjectName = textObjectName;
		}
		
		@Override
		public String getTextObjectName() {
			return textObjectName;
		}
		
		@Override
		public Class<?> getType() {
			return type;
		}
		
		@Override
		public Object create() {
			if (type == null)
				return null;
			
			try {
				return type.newInstance();
			} catch (Exception e) {
				throw new RuntimeException(String.format("can't create element instance. class name %s", type.getName()));
			}
		}
		
		@Override
		public void write(IParsingContext<?> context, Object parent, Object element) {
			try {
				ownAccessor.getWriteMethod().invoke(parent, new Object[] {element});
			} catch (Exception e) {
				throw new RuntimeException(String.format("%s can't set element value", ownAccessor.getName()), e);
			}
		}
	}
	
	private static class ArrayElementAccessor extends DefaultElementAccessor {
		protected static Method writter;
		
		static {
			try {
				writter = List.class.getMethod("add", new Class<?>[] {Object.class});
			} catch (Exception e) {
				throw new RuntimeException("can't get list.add() method!!!");
			}
		}

		public ArrayElementAccessor(Class<?> type) {
			super(type, null);
		}
		
		public void write(IParsingContext<?> context, Object parent, Object element) {
			try {
				writter.invoke(parent, new Object[] {element});
			} catch (Exception e) {
				throw new RuntimeException(String.format("%s can't add element value to array", writter.getName()), e);
			}
		}
	}
	
	private static class TextOnlyElementAccessor extends DefaultElementAccessor {
		private String name;
		private PropertyDescriptor pd;
		
		public TextOnlyElementAccessor(PropertyDescriptor ownAccessor, String name) {
			super(null, ownAccessor);
			this.name = name;
		}
		
		@Override
		public String getTextObjectName() {
			return name;
		}
		
		@Override
		public IPropertyAccessor getPropertyAccessor(String name) {
			if (this.name.equals(name)) {
				return new IPropertyAccessor() {
					
					@Override
					public void write(IParsingContext<?> context, Object object, Object value) {
						Stack<Object> stack =context.getAttribute(KEY_PARSING_OBJECTS_STACK);
						if (stack == null) {
							throw new RuntimeException("null parsing objects stack!!!???");
						}
						
						Object parent = stack.peek();
						if (pd == null) {
							try {
								pd = new PropertyDescriptor(TextOnlyElementAccessor.this.name, parent.getClass());
							} catch (IntrospectionException e) {
								throw new RuntimeException(String.format("can't get property descriptor of %s.%s",
										parent.getClass().getName(), TextOnlyElementAccessor.this.name), e);
							}
						}
						
						try {
							pd.getWriteMethod().invoke(parent, new Object[] {value});
						} catch (Exception e) {
							throw new RuntimeException("can't set text only value", e);
						}
					}
					
				};
			}
			
			return null;
		}
		
		public void write(IParsingContext<?> context, Object parent, Object element) {
		}
	}
	
	private static class TextOnlyArrayElementAccessor extends ArrayElementAccessor {
		private static final String NAME_DEFAULT_TEXT_OBJECT = "text";
		
		public TextOnlyArrayElementAccessor() {
			super(null);
		}
		
		@Override
		public String getTextObjectName() {
			return NAME_DEFAULT_TEXT_OBJECT;
		}
		
		@Override
		public IPropertyAccessor getPropertyAccessor(String name) {
			if (NAME_DEFAULT_TEXT_OBJECT.equals(name)) {
				return new IPropertyAccessor() {

					@SuppressWarnings({ "unchecked", "rawtypes" })
					@Override
					public void write(IParsingContext<?> context, Object object, Object value) {
						Stack<Object> stack =context.getAttribute(KEY_PARSING_OBJECTS_STACK);
						if (stack == null) {
							throw new RuntimeException("null parsing objects stack!!!???");
						}
						
						Object parent = stack.peek();
						if (parent instanceof List) {
							((List)parent).add(value);
						} else {
							throw new RuntimeException("parent isn't a list!!!???");
						}
					}
					
				};
			}
			
			return null;
		}
		
		public void write(IParsingContext<?> context, Object parent, Object element) {
		}
	}
	
	private static interface IPropertyAccessor {
		void write(IParsingContext<?> context, Object object, Object value);
	}
	
	private static class DefaultPropertyAccessor implements IPropertyAccessor {
		private PropertyDescriptor propertyDescriptor;
		
		public DefaultPropertyAccessor(PropertyDescriptor propertyDescriptor) {
			this.propertyDescriptor = propertyDescriptor;
		}

		@Override
		public void write(IParsingContext<?> context, Object object, Object value) {
			try {
				propertyDescriptor.getWriteMethod().invoke(object, new Object[] {value});
			} catch (Exception e) {
				throw new RuntimeException(String.format("can't set property[%s.%s] value ",
						object.getClass().getName(), propertyDescriptor.getName()), e);
			}
		}
		
	}
}
